﻿using System.Net;
using System;
using System.Collections;
using System.Net.Sockets;
using System.Text;

namespace Sockets
{
    class ServerSocket
    {
        #region Member Variables
        private bool connected = false;
        private int connectedClients = 0;
        private int localPort = -1;
        private string localIP = "";
        private IPAddress ipAddress;
        private const int MAX_CONNECTIONS = 10;

        // The Socket that will listen for connections
        Socket listener;

        // An ArrayList of all connected clients
        private ArrayList clients = new ArrayList();
        #endregion

        #region Properties
        /// <summary>
        /// True if the <c>ServerSocket</c> is connected to any clients
        /// </summary>
        public bool Connected
        {
            get
            {
                return connected;
            }
        }

        /// <summary>
        /// Gets or sets the local listening port of the <c>ServerSocket</c>
        /// </summary>
        public int LocalPort
        {
            get
            {
                return localPort;
            }
            set
            {
                if (connected)
                    throw new Exception("Cannot set the local port while the ServerSocket is connected");
                else
                    localPort = value;
            }
        }

        /// <summary>
        /// Gets the local IP of the <c>ServerSocket</c>
        /// </summary>
        public string LocalIP
        {
            get
            {
                return localIP;
            }
        }

        /// <summary>
        /// Returns the number of connected clients
        /// </summary>
        public int ConnectedClients
        {
            get
            {
                return connectedClients;
            }
        }
        #endregion
        
        #region Events
        public delegate void DataReceivedHandler(object sender, DataReceivedEventArgs e);

        /// <summary>
        /// Fired if a client connects
        /// </summary>
        public event EventHandler OnClientConnected;

        /// <summary>
        /// Fired if a client disconnected
        /// </summary>
        public event EventHandler OnClientDisconnected;

        /// <summary>
        /// Fired when a complete message has been received from the remote host
        /// </summary>
        public event DataReceivedHandler OnDataReceived;
        #endregion

        public ServerSocket()
        {
            IPHostEntry ipHostInfo = Dns.Resolve(IPAddress.Any.ToString());  // Deprecated.  I'm still using it... screw IPv6.
            ipAddress = ipHostInfo.AddressList[0];
            localIP = ipAddress.ToString();
        }

        public void Listen()
        {
            if (connected)
                throw new Exception("The ServerSocket is already listening");
            else if (localPort == -1)
                throw new Exception("The remote port has not been set");
            else
            {
                IPEndPoint localEndPoint = new IPEndPoint(ipAddress, localPort);

                // Create a TCP/IP socket.
                listener = new Socket(AddressFamily.InterNetwork,
                    SocketType.Stream, ProtocolType.Tcp);

                listener.Bind(localEndPoint);
                listener.Listen(MAX_CONNECTIONS);
                listener.BeginAccept(new AsyncCallback(AcceptCallback), listener);
            }
        }

        public void send(string message)
        {
            byte[] byteData = Encoding.ASCII.GetBytes(message + "<EOF>");

            // Send to all connected clients
            foreach (Socket client in clients)
            {
                client.BeginSend(byteData, 0, byteData.Length, 0,
                    new AsyncCallback(SendCallback), client);
            }
        }

        public void Disconnect()
        {
            // First we need to stop listening
            listener.Close();

            // Next we need to disconnect all connected clients
            foreach (Socket client in clients)
            {
                client.BeginDisconnect(false, null, null);
            }

            connected = false;
        }

        private void AcceptCallback(IAsyncResult ar)
        {
            // Get the socket that handles the client request.
            Socket theListener = (Socket)ar.AsyncState;

            try
            {
                Socket handler = theListener.EndAccept(ar);
                StateObject state = new StateObject();
                state.workSocket = handler;
                clients.Add(handler);
                connected = true;
                connectedClients++;

                if (OnClientConnected != null)
                    OnClientConnected(this, new EventArgs());

                handler.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0,
                    new AsyncCallback(ReadCallback), state);
                
                // Keep listening
                theListener.BeginAccept(new AsyncCallback(AcceptCallback), listener);
            }
            catch (Exception)
            {
                
            }
        }

        private void ReadCallback(IAsyncResult ar)
        {
            String content = String.Empty;
            int bytesRead = 0;

            // Retrieve the state object and the handler socket
            // from the asynchronous state object.
            StateObject state = (StateObject)ar.AsyncState;
            Socket theHandler = state.workSocket;

            // Read data from the client socket. 
            try
            {
                bytesRead = theHandler.EndReceive(ar);

                if (bytesRead > 0)
                {
                    // There  might be more data, so store the data received so far.
                    state.sb.Append(Encoding.ASCII.GetString(
                        state.buffer, 0, bytesRead));

                    // Check for end-of-file tag. If it is not there, read 
                    // more data.
                    content = state.sb.ToString();

                    if (content.IndexOf("<EOF>") > -1)
                    {
                        content = content.Remove(content.Length - 5, 5);
                        state.sb.Remove(0, state.sb.Length);

                        // All the data has been read from the client
                        if (OnDataReceived != null)
                            OnDataReceived(this, new DataReceivedEventArgs(content));
                    }

                    theHandler.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0,
                        new AsyncCallback(ReadCallback), state);
                }
                else
                {
                    // Client GRACEFULLY disconnected
                    clients.Remove(theHandler);
                    connectedClients--;
                    theHandler.Shutdown(SocketShutdown.Both);
                    theHandler.Close();

                    if (OnClientDisconnected != null)
                        OnClientDisconnected(this, new EventArgs());

                    if (clients.Count == 0)
                        connected = false;
                }
            }
            catch (Exception)
            {
                // Client UNGRACEFULLY disconnected
                clients.Remove(theHandler);
                connectedClients--;
                theHandler.Shutdown(SocketShutdown.Both);
                theHandler.Close();

                if (OnClientDisconnected != null)
                    OnClientDisconnected(this, new EventArgs());

                if (clients.Count == 0)
                    connected = false;
            }
        }

        private void SendCallback(IAsyncResult ar)
        {
            try
            {
                // Retrieve the socket from the state object.
                Socket handler = (Socket)ar.AsyncState;

                // Complete sending the data to the remote device.
                handler.EndSend(ar);
            }
            catch (Exception)
            {
                throw;
            }
        }
    }

    /// <summary>
    /// Used for passing a parameter to subscribers
    /// </summary>
    public class DataReceivedEventArgs : EventArgs
    {
        public DataReceivedEventArgs(string data)
        {
            Data = data;
        }

        public string Data { get; set; }
    }
}
